#include "GLFW.h"
#include "Window.h"
#include "imgui_impl_dx12.h"
#include "Application.h"
#include <d3d12.h>
#include <dxgi1_6.h>
#include <dxgiformat.h>

namespace SL
{

// Data
D3D12_CPU_DESCRIPTOR_HANDLE imgui_handle_cpu;
D3D12_GPU_DESCRIPTOR_HANDLE imgui_handle_gpu;

void Application::createRenderTarget()
{
    for (UINT i = 0; i < APP_NUM_BACK_BUFFERS; i++)
    {
        ID3D12Resource* pBackBuffer = nullptr;
        pSwapChain->GetBuffer(i, IID_PPV_ARGS(&pBackBuffer));
        device->CreateRenderTargetView(pBackBuffer, nullptr, mainRenderTargetDescriptor[i]);
        mainRenderTargetResource[i] = pBackBuffer;
        mainRenderTargetResource[i]->SetName(L"RT_backbuffer");

        D3D12_RESOURCE_DESC resourceDesc = {
            .Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D,
            .Alignment = 0,
            .Width = config.windowDesc.width,
            .Height = config.windowDesc.height,
            .DepthOrArraySize = 1,
            .MipLevels = 1,
            .Format = DXGI_FORMAT_R16G16B16A16_FLOAT,
            .SampleDesc = {1, 0},
            .Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN,
            .Flags = D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET
        };
        D3D12_HEAP_PROPERTIES heapProps = {
            .Type = D3D12_HEAP_TYPE_DEFAULT,
            .CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN,
            .MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN,
            .CreationNodeMask = 1,
            .VisibleNodeMask = 1
        };
        device->CreateCommittedResource(
            &heapProps,
            D3D12_HEAP_FLAG_NONE,
            &resourceDesc,
            D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE,
            nullptr,
            IID_PPV_ARGS(&mainLinearTargetResource[i])
        );
        mainLinearTargetResource[i]->SetName(L"RT_linear_backbuffer");

        D3D12_SHADER_RESOURCE_VIEW_DESC srvDesc = {
            .Format = DXGI_FORMAT_R16G16B16A16_FLOAT,
            .ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D,
            .Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING,
            .Texture2D = {.MostDetailedMip = 0, .MipLevels = 1, .PlaneSlice = 0, .ResourceMinLODClamp = 0}
        };
        srvDescHeapAlloc.Alloc(&mainLinearHandleCPU[i], &mainLinearHandleGPU[i]);
        device->CreateShaderResourceView(mainLinearTargetResource[i], &srvDesc, mainLinearHandleCPU[i]);

        D3D12_RENDER_TARGET_VIEW_DESC rtvDesc = {
            .Format = DXGI_FORMAT_R16G16B16A16_FLOAT,
            .ViewDimension = D3D12_RTV_DIMENSION_TEXTURE2D,
            .Texture2D = {.MipSlice = 0, .PlaneSlice = 0}

        };
        device->CreateRenderTargetView(mainLinearTargetResource[i], &rtvDesc, mainLinearRenderTargetDescriptor[i]);
    }
}

bool Application::createDeviceD3D(HWND hwnd)
{
    // [DEBUG] Enable debug interface
#ifdef DX12_ENABLE_DEBUG_LAYER
    ID3D12Debug* pdx12Debug = nullptr;
    if (SUCCEEDED(D3D12GetDebugInterface(IID_PPV_ARGS(&pdx12Debug))))
        pdx12Debug->EnableDebugLayer();
#endif

    // Create device
    D3D_FEATURE_LEVEL featureLevel = D3D_FEATURE_LEVEL_11_0;
    if (D3D12CreateDevice(nullptr, featureLevel, IID_PPV_ARGS(&device)) != S_OK)
        return false;

        // [DEBUG] Setup debug interface to break on any warnings/errors
#ifdef DX12_ENABLE_DEBUG_LAYER
    if (pdx12Debug != nullptr)
    {
        ID3D12InfoQueue* pInfoQueue = nullptr;
        device->QueryInterface(IID_PPV_ARGS(&pInfoQueue));
        pInfoQueue->SetBreakOnSeverity(D3D12_MESSAGE_SEVERITY_ERROR, true);
        pInfoQueue->SetBreakOnSeverity(D3D12_MESSAGE_SEVERITY_CORRUPTION, true);
        // pInfoQueue->SetBreakOnSeverity(D3D12_MESSAGE_SEVERITY_WARNING, true);
        pInfoQueue->Release();
        pdx12Debug->Release();
    }
#endif
    // descriptor heaps
    {
        // rtv
        D3D12_DESCRIPTOR_HEAP_DESC rtvHeapDesc = {
            .Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV,
            .NumDescriptors = APP_NUM_BACK_BUFFERS * 2,
            .Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE,
            .NodeMask = 1 // ? dont know what this mean
        };
        if (device->CreateDescriptorHeap(&rtvHeapDesc, IID_PPV_ARGS(&rtvDescHeap)) != S_OK)
            return false;

        SIZE_T rtvDescriptorSize = device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
        D3D12_CPU_DESCRIPTOR_HANDLE rtvHandle = rtvDescHeap->GetCPUDescriptorHandleForHeapStart();
        for (UINT i = 0; i < APP_NUM_BACK_BUFFERS; i++)
        {
            mainRenderTargetDescriptor[i] = rtvHandle;
            rtvHandle.ptr += rtvDescriptorSize;
        }
        for (UINT i = 0; i < APP_NUM_BACK_BUFFERS; i++)
        {
            mainLinearRenderTargetDescriptor[i] = rtvHandle;
            rtvHandle.ptr += rtvDescriptorSize;
        }

        // dsv
        D3D12_DESCRIPTOR_HEAP_DESC dsvHeapDesc = {
            .Type = D3D12_DESCRIPTOR_HEAP_TYPE_DSV,
            .NumDescriptors = 1,
            .Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE,
        };
        device->CreateDescriptorHeap(&dsvHeapDesc, IID_PPV_ARGS(&dsvDescHeap));
        // SIZE_T dsvDescriptorSize = device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_DSV);

        // srv
        D3D12_DESCRIPTOR_HEAP_DESC srvHeapdesc = {
            .Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV,
            .NumDescriptors = APP_SRV_HEAP_SIZE,
            .Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE
        };
        if (device->CreateDescriptorHeap(&srvHeapdesc, IID_PPV_ARGS(&srvDescHeap)) != S_OK)
            return false;
        srvDescHeapAlloc.Create(device, srvDescHeap);
    }

    {
        D3D12_COMMAND_QUEUE_DESC desc = {};
        desc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
        desc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;
        desc.NodeMask = 1;
        if (device->CreateCommandQueue(&desc, IID_PPV_ARGS(&directQueue)) != S_OK)
            return false;
    }

    for (UINT i = 0; i < APP_NUM_FRAMES_IN_FLIGHT; i++)
        if (device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&frameContext[i].CommandAllocator)) != S_OK)
            return false;

    if (device->CreateCommandList(
            0, D3D12_COMMAND_LIST_TYPE_DIRECT, frameContext[0].CommandAllocator, nullptr, IID_PPV_ARGS(&mainCommandList)
        ) != S_OK)
        return false;

    mainCommandList->SetName(L"CmdList_main");
    mainCommandList->Close();

    if (device->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&fence)) != S_OK)
        return false;

    fenceEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr);
    if (fenceEvent == nullptr)
        return false;

    // Setup swap chain
    {
        DXGI_SWAP_CHAIN_DESC1 sd;
        ZeroMemory(&sd, sizeof(sd));
        sd.BufferCount = APP_NUM_BACK_BUFFERS;
        sd.Width = 0;
        sd.Height = 0;
        sd.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
        // sd.Flags = DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT;
        sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
        sd.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
        sd.SampleDesc.Count = 1;
        sd.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_TEARING;
        // sd.SampleDesc.Quality = 0;
        // sd.AlphaMode = DXGI_ALPHA_MODE_UNSPECIFIED;
        // sd.Scaling = DXGI_SCALING_STRETCH;
        // sd.Stereo = FALSE;

        IDXGIFactory4* dxgiFactory = nullptr;
        IDXGISwapChain1* swapChain1 = nullptr;
        if (CreateDXGIFactory1(IID_PPV_ARGS(&dxgiFactory)) != S_OK)
            return false;
        if (dxgiFactory->CreateSwapChainForHwnd(directQueue, hwnd, &sd, nullptr, nullptr, &swapChain1) != S_OK)
            return false;
        if (swapChain1->QueryInterface(IID_PPV_ARGS(&pSwapChain)) != S_OK)
            return false;
        swapChain1->Release();
        dxgiFactory->Release();
        // pSwapChain->SetMaximumFrameLatency(APP_NUM_BACK_BUFFERS);
        // pSwapChain->SetMaximumFrameLatency(1);
        // hSwapChainWaitableObject = pSwapChain->GetFrameLatencyWaitableObject();
    }

    createRenderTarget();

    return true;
}

void Application::cleanupRenderTarget()
{
    waitForGpu();

    for (UINT i = 0; i < APP_NUM_BACK_BUFFERS; i++)
    {
        if (mainLinearTargetResource[i])
        {
            mainLinearTargetResource[i]->Release();
        }
        if (mainRenderTargetResource[i])
        {
            mainRenderTargetResource[i]->Release();
            mainRenderTargetResource[i] = nullptr;
        }
    }
}

void Application::cleanupDeviceD3D()
{
    cleanupRenderTarget();
    if (pSwapChain)
    {
        pSwapChain->SetFullscreenState(false, nullptr);
        pSwapChain->Release();
        pSwapChain = nullptr;
    }
    if (depthStencil)
    {
        depthStencil->Release();
        depthStencil = nullptr;
    }
    if (hSwapChainWaitableObject != nullptr)
    {
        CloseHandle(hSwapChainWaitableObject);
    }
    for (UINT i = 0; i < APP_NUM_FRAMES_IN_FLIGHT; i++)
        if (frameContext[i].CommandAllocator)
        {
            frameContext[i].CommandAllocator->Release();
            frameContext[i].CommandAllocator = nullptr;
        }
    if (directQueue)
    {
        directQueue->Release();
        directQueue = nullptr;
    }
    if (mainCommandList)
    {
        mainCommandList->Release();
        mainCommandList = nullptr;
    }
    if (rtvDescHeap)
    {
        rtvDescHeap->Release();
        rtvDescHeap = nullptr;
    }
    if (srvDescHeap)
    {
        srvDescHeap->Release();
        srvDescHeap = nullptr;
    }
    if (dsvDescHeap)
    {
        dsvDescHeap->Release();
        dsvDescHeap = nullptr;
    }
    if (fence)
    {
        fence->Release();
        fence = nullptr;
    }
    if (fenceEvent)
    {
        CloseHandle(fenceEvent);
        fenceEvent = nullptr;
    }
    if (device)
    {
        device->Release();
        device = nullptr;
    }

#ifdef DX12_ENABLE_DEBUG_LAYER
    IDXGIDebug1* pDebug = nullptr;
    if (SUCCEEDED(DXGIGetDebugInterface1(0, IID_PPV_ARGS(&pDebug))))
    {
        pDebug->ReportLiveObjects(DXGI_DEBUG_ALL, DXGI_DEBUG_RLO_ALL);
        pDebug->Release();
    }
#endif
}

Application::Application(const AppConfig& config)
{
    this->config = config;
    m_window = Window::create(config.windowDesc, this);
}

void Application::initializeUI()
{
    // Initialize Direct3D

    // Show the window
    // ::ShowWindow(hwnd, SW_SHOWDEFAULT);
    // ::UpdateWindow(hwnd);

    // Setup Dear ImGui context
    IMGUI_CHECKVERSION();
    ImGui::CreateContext();
    ImGuiIO& io = ImGui::GetIO();
    (void)io;
    io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Enable Keyboard Controls
    // io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad;  // Enable Gamepad Controls
    io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; // Enable Docking
    // io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable;   // Enable Multi-Viewport / Platform Windows
    // io.ConfigViewportsNoAutoMerge = true;
    // io.ConfigViewportsNoTaskBarIcon = true;

    // Setup Dear ImGui style
    ImGui::StyleColorsDark();
    // ImGui::StyleColorsLight();

    // When viewports are enabled we tweak WindowRounding/WindowBg so platform windows can look identical to regular ones.
    ImGuiStyle& style = ImGui::GetStyle();
    if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable)
    {
        style.WindowRounding = 0.0f;
        style.Colors[ImGuiCol_WindowBg].w = 1.0f;
    }

    // Setup Platform/Renderer backends
    ImGui_ImplGlfw_InitForOther(m_window->getWindow(), true);

    ImGui_ImplDX12_InitInfo init_info = {};
    init_info.Device = device;
    init_info.CommandQueue = directQueue;
    init_info.NumFramesInFlight = APP_NUM_FRAMES_IN_FLIGHT;
    init_info.RTVFormat = DXGI_FORMAT_R8G8B8A8_UNORM;
    init_info.DSVFormat = DXGI_FORMAT_R32_FLOAT;
    // Allocating SRV descriptors (for textures) is up to the application, so we provide callbacks.
    // (current version of the backend will only allocate one descriptor, future versions will need to allocate more)

    // pre-allocate one descriptor to avoid capturing lambda
    srvDescHeapAlloc.Alloc(&imgui_handle_cpu, &imgui_handle_gpu);

    init_info.SrvDescriptorHeap = srvDescHeap;
    init_info.SrvDescriptorAllocFn =
        [](ImGui_ImplDX12_InitInfo*, D3D12_CPU_DESCRIPTOR_HANDLE* out_cpu_handle, D3D12_GPU_DESCRIPTOR_HANDLE* out_gpu_handle)
    {
        *out_cpu_handle = imgui_handle_cpu;
        *out_gpu_handle = imgui_handle_gpu;
    };
    init_info.SrvDescriptorFreeFn =
        [](ImGui_ImplDX12_InitInfo*, D3D12_CPU_DESCRIPTOR_HANDLE cpu_handle, D3D12_GPU_DESCRIPTOR_HANDLE gpu_handle) { // do nothing
        };
    ImGui_ImplDX12_Init(&init_info);
}

void Application::initializeBackend()
{
    // Initialize Direct3D
    HWND hwnd = glfwGetWin32Window(m_window->getWindow());
    if (!createDeviceD3D(hwnd))
    {
        cleanupDeviceD3D();
        m_window->shutDown();
        // ::UnregisterClassW(wc.lpszClassName, wc.hInstance);
        throw std::runtime_error("Failed to create device D3D");
        // return 1;
    }
}

void Application::initialize()
{
    if (initialized)
    {
        return;
    }
    initializeBackend();
    initializeUI();
    initialized = true;
}

void Application::shutDown()
{
    shutDownUI();
    shutDownBackend();
}

void Application::shutDownBackend()
{
    m_window->shutDown();
    cleanupDeviceD3D();
}

void Application::shutDownUI()
{
    ImGui_ImplDX12_Shutdown();
    // free the pre-allocated imgui handle
    srvDescHeapAlloc.Free(imgui_handle_cpu, imgui_handle_gpu);
    ImGui_ImplGlfw_Shutdown();
    ImGui::DestroyContext();
}

void Application::fixedUpdate()
{
    onFixedUpdate();
}

void Application::update(float dt)
{
    onUpdate(dt);
}

void Application::run()
{
    initialize();

    onLoad();

    float delta_t = 1.0f; // / m_window->getRefreshRate();

    std::chrono::steady_clock::time_point start = std::chrono::high_resolution_clock::now();
    std::chrono::steady_clock::time_point end;

    // msgloop
    while (!m_window->shouldClose())
    {
        m_window->pollEvents();
        // profiler begin

        fixedUpdate();

        end = std::chrono::high_resolution_clock::now();
        delta_t = std::chrono::duration<float, std::milli>(end - start).count(); // ms
        start = end;
        update(delta_t);

        renderFrame();

        // profiler end
    }

    waitForGpu();

    onShutdown();

    shutDown();
}

void Application::renderFrame()
{
    // prepare UI data
    updateUI();

    onFrameRender();

    directQueue->ExecuteCommandLists(1, (ID3D12CommandList* const*)&mainCommandList);

    updateAndRenderUIMultipleWindows();

    // Present
    if (config.windowDesc.enableVSync)
        pSwapChain->Present(1, 0); // with vsync
    else
        pSwapChain->Present(0, DXGI_PRESENT_ALLOW_TEARING);

    moveToNextFrame();
}

void Application::updateAndRenderUIMultipleWindows()
{
    // Update and Render additional Platform Windows
    ImGuiIO& io = ImGui::GetIO();
    if (bShowUI && io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable)
    {
        // create/update/destroy platform (native OS) windows
        ImGui::UpdatePlatformWindows();
        // backend draw other windows
        ImGui::RenderPlatformWindowsDefault();
    }
}

void Application::updateUI()
{
    if (bShowUI)
    {
        // dx12 backends (devices)
        // resets per-frame internal data related to command lists and graphics resources
        ImGui_ImplDX12_NewFrame();
        // update window state, input, etc
        ImGui_ImplGlfw_NewFrame();
        // resets internal state and allows you to build your GUI elements
        ImGui::NewFrame();

        // create dockspace
        // ImGuiID dockspace_id = ImGui::GetID("mainViewPort");
        // ImGui::DockSpace(dockspace_id, ImVec2(1280.f, 720.f), ImGuiDockNodeFlags_None);

        // Render your GUI elements here
        onGuiRender();

        // Converts all UI commands into a renderable `ImDrawData`
        ImGui::Render();
    }
}

void Application::renderUI()
{
    // vertexs, draw calls, root signature, pso, etc
    if (bShowUI)
        ImGui_ImplDX12_RenderDrawData(ImGui::GetDrawData(), mainCommandList);
}

void Application::moveToNextFrame()
{
    // Schedule a Signal command in the queue.
    const UINT64 currentFenceValue = fenceValues[frameIndex];
    directQueue->Signal(fence, currentFenceValue);

    // Update the frame index.
    frameIndex = pSwapChain->GetCurrentBackBufferIndex();

    // If the next frame is not ready to be rendered yet, wait until it is ready.
    if (fence->GetCompletedValue() < fenceValues[frameIndex])
    {
        fence->SetEventOnCompletion(fenceValues[frameIndex], fenceEvent);
        WaitForSingleObjectEx(fenceEvent, INFINITE, FALSE);
    }

    // Set the fence value for the next frame.
    fenceValues[frameIndex] = currentFenceValue + 1;
}
void Application::waitForGpu()
{
    // Schedule a Signal command in the queue.
    directQueue->Signal(fence, fenceValues[frameIndex]);

    // Wait until the fence has been processed.
    fence->SetEventOnCompletion(fenceValues[frameIndex], fenceEvent);
    WaitForSingleObjectEx(fenceEvent, INFINITE, FALSE);

    // Increment the fence value for the current frame.
    fenceValues[frameIndex]++;
}

void Application::handleKeyboardEvent(const KeyboardEvent& keyEvent)
{
    // if (bShowUI && mpGui->onKeyboardEvent(keyEvent))
    if (bShowUI && ImGui::GetIO().WantCaptureKeyboard)
        return;
    // mInputState.onKeyEvent(keyEvent);
    onKeyboardEvent(keyEvent);

    // Checks if should toggle zoom
    // mpPixelZoom->onKeyboardEvent(keyEvent);

    // Consume system messages first
    if (keyEvent.type == KeyboardEvent::Type::KeyPressed)
    {
        //        if (keyEvent.hasModifier(Input::Modifier::Ctrl))
        //        {
        //            switch (keyEvent.key)
        //            {
        //            case Input::Key::Pause:
        //            case Input::Key::Space:
        //                mRendererPaused = !mRendererPaused;
        //                break;
        //            default:
        //                break;
        //            }
        //        }
        if (keyEvent.mods == Input::ModifierFlags::None)
        {
            switch (keyEvent.key)
            {
                //            case Input::Key::F12:
                //                mCaptureScreen = true;
                //                break;
                // #if FALCOR_ENABLE_PROFILER
                //            case Input::Key::P:
                //                getDevice()->getProfiler()->setEnabled(!getDevice()->getProfiler()->isEnabled());
                //                break;
                // #endif
                //            case Input::Key::V:
                //                // TODO(@skallweit) we'd need to recreate the swapchain here
                //                mVsyncOn = !mVsyncOn;
                //                mFrameRate.reset();
                //                mClock.setTime(0);
                //                break;
            case Input::Key::F2:
                //    toggleUI(!mShowUI);
                bShowUI = !bShowUI;
                break;
                //    case Input::Key::F5:
                //    {
                //        HotReloadFlags reloaded = HotReloadFlags::None;
                //        if (mpDevice->getProgramManager()->reloadAllPrograms())
                //            reloaded |= HotReloadFlags::Program;
                //        onHotReload(reloaded);
                //    }
                //    break;
                //    case Input::Key::Escape:
                //        mpWindow->shutdown();
                //        break;
                //    case Input::Key::Pause:
                //    case Input::Key::Space:
                //        mClock.isPaused() ? mClock.play() : mClock.pause();
                //        break;
            default:
                break;
            }
        }
    }
}

void Application::handleMouseEvent(const MouseEvent& mouseEvent)
{
    if (bShowUI && ImGui::GetIO().WantCaptureMouse)
        return;
    // mInputState.onMouseEvent(mouseEvent);
    if (onMouseEvent(mouseEvent))
        return;
    // if (mpPixelZoom->onMouseEvent(mouseEvent))
    //     return;
}

void Application::handleGamepadEvent(const GamepadEvent& gamepadEvent)
{
    onGamepadEvent(gamepadEvent);
}

void Application::handleGamepadState(const GamepadState& gamepadState)
{
    onGamepadState(gamepadState);
}

void Application::handleDroppedFile(const std::filesystem::path& path)
{
    onDroppedFile(path);
}

void Application::handleWindowSizeChange()
{
    onResize();
}
} // namespace SL